/*
 * Copyright (C) Citypassenger Ltd. All rights reserved.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License version 3.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * In addition, as a special exception, the copyright holders give
 * permission to link the code of portions of this program with the
 * OpenSSL library under certain conditions as described in each
 * individual source file, and distribute linked combinations
 * including the two.
 *
 * You must obey the GNU Affero General Public License in all respects
 * for all of the code used other than OpenSSL.
 */

#include "monit.h"
#include "cervlet.h"  
#include "engine.h"
#include "file_list_loader.h"

//for save_new_conf
#include <sys/types.h>
#include <sys/uio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h> //munmap
#include <string.h>

List_T conf_files = NULL;
List_T current_files = NULL;

static const char* ta_style = "height: 25em;"
                              "width: 98%;"
                              "margin: 1%;"
                              "resize: vertical;"
                              "color: #111;"
                              "background-color: #fdfdfd";

/*
confs file management api
*/
//create .old file
static int save_new_conf(const char*, const char*);
//just update the current (.old is still here)
static int update_new_conf(const char*, const char*);
//remove .old file (put it back)
static int restor_conf(const char*, const char*);
//remove .old file, question? level one should be able to do that... (ro user)
static int validate_conf(const char*);
//when booting if old file is present,
//if marked , restore else marked
static int mark_or_restore(Service_T s);
/*
dists confs file management api
*/
static int dist_restor_conf();
static int dist_backup_conf();

/*
confs file management constant
*/
#define CONF_ACTION "action_conf"
#define CONF_RESTOR "Restore previous"
#define CONF_VALID  "Validate"
#define CONF_SAVE   "Save"
#define CONF_DIST_BACKUP "Backup"
#define CONF_DIST_RESTOR "Restore"

//FIXME: return null if not enough memmory
void confs_init() {
        Service_T ls = servicelist;
        if (conf_files == NULL) {
                conf_files = List_new();
                current_files = List_new(); //TODO:deprecated
                for (; ls; ls = ls->next)
                        mark_or_restore(ls);
        }
}


struct conf_process {
        char* path;
        struct file_list_entry* d;
        int done;
};


void find_current_edit(void *e, void *ap) {
        struct file_list_entry* fle = e;
        struct conf_process* cp = ap;
        if (cp->done == 0 && strcmp(fle->name,cp->path) == 0) {
                cp->done = 1;
                cp->d = fle;
        }
}

char* get_conf_content(const char* file_path, struct file_list_entry** pfle) {
        struct conf_process cp;
        struct file_list_entry* fle;
        cp.path = (char*)file_path;
        cp.done = 0;
        List_map(current_files, find_current_edit, &cp);
        if (cp.done == 0) {
                List_map(conf_files, find_current_edit, &cp);
        } else {
                LogDebug("%s: found current conf: %s \n", prog, file_path);
                int rc = sync_file((char*)file_path, &cp.d);
                if (rc) {
                        LogInfo("%s: cannot sync conf: %s \n", prog, file_path);
                        List_remove(current_files, cp.d);
                        List_remove(conf_files, cp.d);
                        FREE(cp.d);
                        return NULL;
                }
        }
        if (cp.done == 0) {
                int rc = open_file((char*)file_path, &fle);
                if (rc) {
                        LogInfo("%s: cannot open conf: %s \n", prog, file_path);
                        return NULL;
                } else {
                        List_push(conf_files, fle);
                        cp.d = fle;
                }
        } else {
                LogDebug("%s: found normal conf: %s \n", prog, file_path);
                int rc = sync_file((char*)file_path, &cp.d);
                if (rc) {
                        LogInfo("%s: cannot sync normal conf: %s \n", prog, file_path);
                        List_remove(conf_files, cp.d);
                        FREE(cp.d);
                        return NULL;
                } else
                        List_push(current_files, cp.d);
        }
        if (pfle) *pfle = cp.d;
        return cp.d->map;
}

static int is_old_conf_present(char* file_path) {
        struct stat soc;
        int ret = 0;
        const char* soc_path;
        StringBuffer_T oldconfs = StringBuffer_new(file_path);
        StringBuffer_append(oldconfs,".old");
        soc_path = StringBuffer_toString(oldconfs);
        if (0 == stat(soc_path, &soc)) {
              if (!S_ISREG(soc.st_mode))
                      LogInfo("%s: old confs file is not regular %s\n", prog, soc_path);
              ret = 1;
        }
        StringBuffer_free(&oldconfs);
        return ret;
}

static int is_mark_conf_present(char* file_path) {
        struct stat soc;
        int ret = 0;
        const char* soc_path;
        StringBuffer_T oldconfs = StringBuffer_new(file_path);
        StringBuffer_append(oldconfs,".mark");
        soc_path = StringBuffer_toString(oldconfs);
        LogDebug("%s: stat(%s)\n", "is_mark_conf_present", soc_path);
        if (0 == stat(soc_path, &soc)) {
              if (!S_ISREG(soc.st_mode))
                      LogInfo("%s: old confs file is not regular %s\n", prog, soc_path);
              ret = 1;
        }
        StringBuffer_free(&oldconfs);
        LogDebug("%s: return(%d)\n", "is_mark_conf_present", ret);
        return ret;
}

static void append_confs_button(HttpResponse res,
                   const char* function) {
        StringBuffer_append(res->outputbuffer, 
                           "<input type=submit value='%s' name='%s'>",
                           function, CONF_ACTION);
}

void print_confs(HttpRequest req, HttpResponse res, Service_T s, int ro) {
        Config_T c;
        StringBuffer_append(res->outputbuffer,
                            "<tr><td colspan='2' style='text-align:left;'>"
                            "Configuration Files : </td></tr>");
        for(c= s->configlist; c; c= c->next) {
                if(c->path != NULL) { //this was asserted , but well, better safe than sorry file:p.y, line:2207
                        struct file_list_entry* pfle = NULL;
                        if (NULL == get_conf_content(c->path, &pfle)) continue;
                        StringBuffer_append(res->outputbuffer,"<tr><td style='vertical-align:top;'>%s</td><td>", c->path);
                        StringBuffer_append(res->outputbuffer,
                                              "<form method=POST action='%s' enctype='multipart/form-data'>"
                                              "<input type=hidden value='%s' name='path' >"
                                              "<input type=hidden value='%s' name='action' >"
                                              "<textarea %s name='file' style='%s' wrap='off'>",
                                              s->name, c->path, CONF_ACTION, ro ? "disabled" : "", ta_style);
                        StringBuffer_append_nstr(res->outputbuffer, pfle->map, pfle->size);
                        StringBuffer_append(res->outputbuffer,"</textarea>");
                        if(!ro) {
                                //Safe modif api
                                StringBuffer_append(res->outputbuffer,"<br><span>");
                                append_confs_button(res, CONF_SAVE);
                                StringBuffer_append(res->outputbuffer,"</span><span>");
                                if (is_old_conf_present(c->path)) {
                                        append_confs_button(res, CONF_RESTOR);
                                        StringBuffer_append(res->outputbuffer,"</span>");
                                        //only after mark
                                        //note, reloading the service may create the mark :)
                                        if (is_mark_conf_present(c->path)) {
                                                StringBuffer_append(res->outputbuffer,"<span>");
                                                append_confs_button(res, CONF_VALID);
                                        }
                                }
                                StringBuffer_append(res->outputbuffer,"</span><span><input type='reset'></span>");
                                //Storage api
                                /* Not active YET, backup would be more backup the all data for the entry
                                //like the sql base for mysql service
                                if (Run.storage != NULL) {
                                        StringBuffer_append(res->outputbuffer,"<hr>");
                                        StringBuffer_append(res->outputbuffer,"<span>");
                                        append_confs_button(res, CONF_DIST_BACKUP);
                                        StringBuffer_append(res->outputbuffer,"</span><span>");
                                        append_confs_button(res, CONF_DIST_RESTOR);
                                        StringBuffer_append(res->outputbuffer,"</span>");
                                }
                                */
                        }
                        StringBuffer_append(res->outputbuffer, "</form>");
                        StringBuffer_append(res->outputbuffer,"</td></tr>");
                }
        }
}

static const char* get_long_parameter(HttpRequest req , const char* name) {
        HttpParameter p;
        for(p= req->lparams; p; p= p->next) {
                if(!strcasecmp(p->name, name)) {
                        return (p->value);
                }
        }
        return NULL;
}

//callback when button press
int handle_conf_action(HttpRequest req, HttpResponse res) {
        int iret = 1;
        LogDebug("%s: handle_conf_action \n", prog);

        const char *action = get_long_parameter(req, "action");
        LogDebug("%s: handle_conf_action: %s \n", prog, action);
        if (! IS(action, CONF_ACTION)) {
                  return 0;
        }
        const char *value = get_long_parameter(req, "file");
        const char *p = get_long_parameter(req, "path");
        const char *action_name = get_long_parameter(req, action);
        const char* soc_path = NULL;
        StringBuffer_T cc = StringBuffer_new(p);
        soc_path = StringBuffer_toString(cc);
        LogDebug("%s: handle_conf_action: %s(%s)%s \n", prog, action, value, action_name);
        
        if (IS(action_name, CONF_SAVE)) {
                LogDebug("%s: saving: %s(%s) \n", prog, action, soc_path);
                if (is_old_conf_present((char*)soc_path)) {
                        update_new_conf(soc_path, value);
                } else {
                        save_new_conf(soc_path, value);
                }
        } else if (IS(action_name, CONF_RESTOR)) {
                LogDebug("%s: restore from .old: %s \n", prog, soc_path);
                StringBuffer_T cold = StringBuffer_new(soc_path);
                StringBuffer_append(cold, ".old");
                restor_conf(soc_path, StringBuffer_toString(cold));
                StringBuffer_free(&cold);
        } else if (IS(action_name, CONF_VALID)) {
                LogDebug("%s: valid: %s \n", prog, p);
                validate_conf(soc_path);
        } else if (IS(action_name, CONF_DIST_BACKUP)) {
                LogDebug("%s: dist backup: %s \n", prog, p);
        } else if (IS(action_name, CONF_DIST_RESTOR)) {
                LogDebug("%s: dist restor: %s \n", prog, p);
        } else
                iret = 0;
end:
        StringBuffer_free(&cc);
        return iret;
}


/*
confs implements
*/
static int mark_or_restore(Service_T s) {
        struct stat soc;
        int fd;
        Config_T c;
        const char* c_mark, *c_old;
        for(c= s->configlist; c; c= c->next) {
                LogDebug("%s: mark_or_restore DO (%s) \n",
                          prog, c->path);
                StringBuffer_T mark = StringBuffer_new(c->path);
                StringBuffer_T old = StringBuffer_new(c->path);
                StringBuffer_append(mark,".mark");
                StringBuffer_append(old,".old");
                c_mark = StringBuffer_toString(mark);
                c_old = StringBuffer_toString(old);
                if ( 0 == stat( c_mark, &soc) ) {
                        //put back old
                        restor_conf(c->path, c_old);
                } else {
                        if ( 0 == stat( c_old, &soc) ) {
                                fd = open(c_mark, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
                                if (fd < 0) {
                                        LogError("%s: mark_or_restore, cannot create mark: (%s) \n",
                                                  prog, c_mark);
                                } else {
                                        LogDebug("%s: mark_or_restore Create mark (%s) \n",
                                                  prog, c->path);
                                        close(fd);
                                }
                        }
                }
                StringBuffer_free(&mark);
                StringBuffer_free(&old);
        }
        return 1;
}


//calllback for list_map in push_new_in_list
void remap_new(void *e, void *ap) {
        struct file_list_entry* fle = e;
        struct file_list_entry* newfle;
        struct conf_process* cp = ap;
        if (cp->done != 0) return;
        if (strcmp(fle->name,cp->path) == 0) {
                int rc = open_file(cp->path, &newfle);
                if (rc) {
                        LogInfo("%s: cannot open conf: %s \n", "remap_new", fle->name);
                        cp->done = 2;
                        return;
                } else {
                        LogDebug("%s: validate_conf remaping (%s) \n",
                                  "remap_new", cp->path);
                        close(fle->fd);
                        munmap(fle->map, fle->size);
                        *fle = *newfle;
                        FREE(newfle);
                        cp->done = 1; //means ok
                }
        } else {
                LogDebug("%s: skipping (%s)==(%s) \n",
                          "remap_new", fle->name,cp->path );
        }
}

//if current confs chnage the mapping in the file list must be updated
static int push_new_in_list(const char* path) {
        struct conf_process cp;
        cp.path = (char*)path;
        cp.done = 0;
        List_map(current_files, remap_new, &cp);
        if (cp.done != 1) {
                cp.done = 0;
                LogError("%s: cannot map new configuration: (%s) \n",
                          prog, path);
        } else {
                LogDebug("%s: validate_conf remaping (%s) \n",
                          "save_new_conf", cp.path);
        }
        return cp.done;
}

//remove .old file (put it back)
static int restor_conf(const char* path, const char* old_path) {
        StringBuffer_T mark = StringBuffer_new(path);
        StringBuffer_append(mark,".mark");
        int i = rename(old_path, path);
        if (i) {
                LogError("%s: cannot restore old conf: (%s) \n",
                          prog, path);
        } else {
                LogDebug("%s: restor_conf OK (%s) \n",
                          prog, path);
        }
        if (push_new_in_list(path) == 0) {
                LogError("%s: cannot update list for restor: (%s) \n",
                          prog, path);
        }
        unlink(StringBuffer_toString(mark)); // deliberatly ignore error
        StringBuffer_free(&mark);
        return !i;
}

//remove .old file, question? level one should be able to do that... (ro user), remove mark too
static int validate_conf(const char* path) {
        int iret = 1;
        StringBuffer_T mark = StringBuffer_new(path);
        StringBuffer_T old = StringBuffer_new(path);
        StringBuffer_append(mark,".mark");
        StringBuffer_append(old,".old");
        if (unlink(StringBuffer_toString(mark))) {
                LogError("%s: cannot remove mark on conf: (%s) \n",
                          prog, path);
                iret = 0;
        } else {
                LogDebug("%s: validate_conf OK (%s) \n",
                          prog, StringBuffer_toString(mark));
        }
        if (unlink(StringBuffer_toString(old))) {
                LogError("%s: cannot remove old conf: (%s) \n",
                          prog, StringBuffer_toString(old));
                iret = 0;
        } else {
                LogDebug("%s: validate_conf OK (%s) \n",
                          prog, old);
        }
        StringBuffer_free(&mark);
        StringBuffer_free(&old);
        return iret;
}

static int write_new_conf(const char* path,const char* new_content) {
        int iret = 0;
        int new_length = strlen(new_content);
        int i = 0;
        struct stat old_file_info;
        StringBuffer_T new_name = StringBuffer_new(path);
        StringBuffer_append(new_name,".new");
        const char* c_new_name = StringBuffer_toString(new_name);
        int new_fd = open(c_new_name, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
        if (new_fd < 0) {
                LogError("%s: cannot create configuration: (%s) \n",
                          prog, c_new_name);
                goto end;
        }
        LogDebug("%s: create file ok: (%s) \n",
                  "write_new_conf", path);
        //Assume configuration are EOL=\n files
        for (i = 0; i < new_length; ++i) {
          if (new_content[i] == '\r') {
            if ( i < new_length - 1 && new_content[i + 1] == '\n') {
              continue;
            } else {
              ((char*)new_content)[i] = '\n';
            }
          }
          if (1 != write(new_fd, &(new_content[i]), sizeof(char)) ) {
            LogError("%s: cannot write new configuration: (%s) \n",
                      prog, path);
            goto end;
          }
        }
        LogDebug("%s: writing file ok: (%s) \n",
                  "write_new_conf", path);
        i = stat(path,&old_file_info);
        if (i == -1) {
            LogError("%s: cannot read old right access configuration: (%s) \n",
                      prog, path);
        } else {
            i = fchmod(new_fd, old_file_info.st_mode);
            if (i == -1) LogError("%s: cannot write old right access configuration: (%s) \n",
                                  prog, path);
            i = fchown(new_fd, old_file_info.st_uid, old_file_info.st_gid);
            if (i == -1) LogError("%s: cannot write old owner access configuration: (%s) \n",
                                  prog, path);
        }
        if (rename(c_new_name, path)) {
                LogError("%s: cannot apply new configuration: (%s) \n",
                          prog, path);
                goto end;
        } else {
                LogDebug("%s: apply new configuration: (%s) \n",
                          "save_new_conf", c_new_name);
        }
        iret = 1;
end:
        if (new_fd != -1) close(new_fd);
        StringBuffer_free(&new_name);
        return iret;
}

static int write_old_conf(const char* path,const char* current_content, int size) {
        int iret = 0;
        StringBuffer_T old_name = StringBuffer_new(path);
        StringBuffer_append(old_name,".old");
        const char* c_old_name = StringBuffer_toString(old_name);
        int old_fd = open(c_old_name, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
        if (old_fd < 0) {
                LogError("%s: cannot save configuration: (%s) \n",
                          prog, c_old_name);
                goto end;
        }
        int rc_old = write(old_fd, current_content, size);
        if (rc_old != size) {
                LogError("%s: cannot write old configuration: (%s) \n",
                          prog, path);
                goto end;
        }
        iret = 1;
end:
        if (old_fd != -1) close(old_fd);
        StringBuffer_free(&old_name);
        return iret;
}

static int update_new_conf(const char* path, const char* new_content) {
        int rc = write_new_conf(path, new_content);
        if (rc == 0) return 0;
        LogDebug("%s: push_new_in_list\n", "update_new_conf");
        rc = push_new_in_list(path);
        return rc;
}

static int save_new_conf(const char* path, const char* new_content) {
        struct file_list_entry* fle;
        char* current_content = get_conf_content(path, &fle);
        int rc = write_old_conf(path, current_content, fle->size);
        if (rc == 0) return 0;
        rc = write_new_conf(path, new_content);
        if (rc == 0) return 0;
        LogDebug("%s: push_new_in_list\n", "save_new_conf");
        rc = push_new_in_list(path);
        return rc;
}
